package smallfrench.moteur.biblio;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import smallfrench.moteur.biblio.err.AException;
import smallfrench.moteur.biblio.err.ErreurClasse;
import smallfrench.moteur.biblio.err.ErreurParametres;
import smallfrench.moteur.interp.RObjet;
import smallfrench.moteur.interp.err.RException;

/**
 * Classe immuable, représentant la classe d’un objet smallFrench complexe,
 * pouvant être facilement étendu.
 * @author antoine1023
 */
public final class AClasse extends RClasse {

	/**
	 * Map des classes Java vers les classes sF correspondantes.
	 */
	private static Map<Class<?>, AClasse> classesJavaVersSF;

	static {
		classesJavaVersSF = new HashMap<Class<?>, AClasse>();
	}

	//------------------------------------------------------

	/**
	 * Créé une nouvelle {@link AClasse} à partir d’une classe Java.
	 * @param classeJava
	 * @return La nouvelle classe
	 */
	public static AClasse creer(Class<?> classeJava) {
		return creer(classeJava, null);
	}

	/**
	 * Créé une nouvelle {@link AClasse} à partir d’une classe Java.
	 * 
	 * @param classeJava
	 * @param nom Le nom de la future classe ou <code>null</code> si le nom de
	 * la future classe est le même que celui de la classe Java.
	 * @return La nouvelle classe
	 */
	public static AClasse creer(Class<?> classeJava, String nom) {

		// On vérifie que la classe hérite bien de AObjet
		if (! AObjet.class.isAssignableFrom(classeJava))
			throw new RuntimeException(classeJava + " doit hériter de AObjet");

		// On vérifie que l’AClasse correspondante n’est pas déjà répertoriée
		if (classesJavaVersSF.containsKey(classeJava))
			throw new RuntimeException();

		if (nom == null)
			nom = classeJava.getSimpleName();

		Class<?> superClass = classeJava.getSuperclass();
		RClasse parente;

		if (superClass == AObjet.class) {

			parente = RClasse.CLASSE;

		} else {

			parente = classesJavaVersSF.get(superClass);

			if (parente == null)
				throw new RuntimeException();
		}

		RClasse[] parentes = new RClasse[] {parente};

		boolean estAbstraite = Modifier.isAbstract(
				classeJava.getModifiers());

		// On récupère le constructeur
		RFonction constructeur = getConstructeur(classeJava);

		if (constructeur == null && ! estAbstraite) {
			throw new RuntimeException("Une classe sans constructeur "
					+ "avec l’annotation @AnnotationMembre doit être "
					+ "abstraite.");
		}

		HashMap<String, RObjet> champsStatiques = getMethodes(classeJava, true);

		HashMap<String, RObjet> methodes = getMethodes(classeJava, false);

		AClasse aclasse = new AClasse(
				parentes,
				constructeur,
				methodes,
				champsStatiques,
				estAbstraite,
				classeJava,
				nom);

		// On s’ajoute à la map
		classesJavaVersSF.put(classeJava, aclasse);

		return aclasse;
	}


	/**
	 * 
	 * @param classeJava
	 * @return Le premier constructeur de la classe annoté de
	 * {@link AnnotationMembre} ou <code>null</code>.
	 */
	private static AConstructeur getConstructeur(Class<?> classeJava) {

		Constructor<?>[] ctors = classeJava.getConstructors();

		for (Constructor<?> i : ctors) {
			if (i.isAnnotationPresent(AnnotationMembre.class)) {
				AnnotationMembre am = i.getAnnotation(AnnotationMembre.class);
				AConstructeur constructeur = new AConstructeur(
						i, am.parametresVariables());
				return constructeur;
			}
		}

		return null;
	}


	/**
	 * 
	 * @param classe
	 * @param statiques Si <code>true</code>, ne rassemble que les méthodes
	 * statiques. Si <code>false</code>, ne rassemble que les méthodes
	 * non statiques.
	 * 
	 * @return Une {@link HashMap} des méthodes annotées de l’annotation
	 * {@link AnnotationMembre}. Les entrées des méthodes abstraites ont pour
	 * valeur {@link RRien#RIEN}.
	 */
	private static HashMap<String, RObjet> getMethodes(Class<?> classe,
			boolean statiques) {

		HashMap<String, RObjet> map = new HashMap<String, RObjet>();

		for (Method methodeJava : classe.getDeclaredMethods()) {

			if (methodeJava.isAnnotationPresent(AnnotationMembre.class)) {

				AnnotationMembre a = methodeJava.getAnnotation(
						AnnotationMembre.class);

				String nom = a.nom();
				if (nom.length() == 0)
					nom = methodeJava.getName();

				if (Modifier.isAbstract(methodeJava.getModifiers())) {
					map.put(nom, RRien.RIEN);
				} else {

					AMethode methode = new AMethode(methodeJava,
							a.parametresVariables());

					boolean ms = Modifier.isStatic(methodeJava.getModifiers());
					if (statiques == ms) {
						map.put(nom, methode);
					}
				}
			}
		}

		return map;
	}


	/**
	 * @param classeJava
	 * @return L’objet {@link AClasse} correspondant.
	 * @throws IllegalArgumentException si la classe correspondante n’est pas
	 * répertoriée.
	 */
	public static AClasse getClasseSF(Class<?> classeJava) {
		if (! classesJavaVersSF.containsKey(classeJava))
			throw new IllegalArgumentException(
					"Classe non trouvée : " + classeJava);
		return classesJavaVersSF.get(classeJava);
	}

	/**
	 * 
	 * @param ctor
	 * @return Une chaine décrivant le nom du constructeur où l’erreur a
	 * eu lieu.
	 */
	private static String getDescrErreurConstructeur(Constructor<?> ctor) {
		return "Erreur interne lors de l’appel de "
				+ ctor.getDeclaringClass().getName() + "."
				+ ctor.getName() + "()";
	}



	/**
	 * @param method
	 * @return Une chaine décrivant le nom de la méthode où l’erreur a
	 * eu lieu.
	 */
	private static String getDescrErreurMethode(Method method) {
		return "Erreur interne lors de l’appel de "
				+ method.getDeclaringClass().getName() + "."
				+ method.getName() + "()";
	}


	/**
	 * Convertit un objet Java en un objet sF.
	 * @param o
	 * @return L’objet o converti
	 */
	public static RObjet javaVersSF(Object o) {

		if (o == null)
			return RRien.RIEN;

		else if (o instanceof RObjet)
			return (RObjet) o;

		else if (o instanceof String)
			return new RChaine((String) o);

		else if (o instanceof Integer)
			return new REntier((Integer) o);

		else if (o instanceof Long)
			return new REntier((Long) o);

		else if (o instanceof Float)
			return new RFlottant((Float) o);

		else if (o instanceof Double)
			return new RFlottant((Double) o);

		// else if (o instanceof List<?>)

		else
			throw new RuntimeException("Impossible de convertir " + o +
					" en objet SF");
	}



	/**
	 * La classe Java correspondante ou <code>null</code>, si la classe est
	 * écrite en smallFrench.
	 */
	public final Class<?> classeJava;



	/**
	 * Constructeur ou <code>null</code> si la classe est native et n’est pas
	 * instanciable par un script, ou si la classe a un constructeur vide.
	 */
	private final RObjet constructeur;

	//	/**
	//	 * @param nom
	//	 * @return La méthode correspondante ou <code>null</code> si non trouvée
	//	 * ou abstraite.
	//	 */
	//	public IFonction getMethodeDeclaree(String nom) {
	//		return methodes.get(nom);
	//	}

	//	/**
	//	 * @param nom
	//	 * @return <code>true</code> si la méthode est abstraite.
	//	 */
	//	public boolean methodeEstAbstraite(String nom) {
	//		return methodes.get(nom) == null;
	//	}

	//	/**
	//	 * @param nom
	//	 * @return <code>true</code> si la méthode est définie (elle peut être
	//	 * abstraite)
	//	 */
	//	public boolean methodeEstDefinie(String nom) {
	//		return methodes.containsKey(nom);
	//	}

	/**
	 * Nom de la classe
	 */
	public final String nom;


	/**
	 * Constructeur privé.
	 * Créée une nouvelle classe native.
	 * 
	 * @param parentes
	 * @param constructeur
	 * @param methodes
	 * @param champsStatiques
	 * @param estAbstraite
	 * @param classeJava
	 * @param nom
	 */
	private AClasse(
			RClasse[] parentes,
			RObjet constructeur,
			HashMap<String, RObjet> methodes,
			HashMap<String, RObjet> champsStatiques,
			boolean estAbstraite,
			Class<?> classeJava,
			String nom) {

		super(parentes, methodes, champsStatiques, estAbstraite, true);
		this.constructeur = constructeur;
		this.nom = nom;
		this.classeJava = classeJava;
	}

	/**
	 * Crée une nouvelle classe non intrinsèque.
	 * 
	 * @param nom Le nom de la classe.
	 * @param parentes Les superclasses.
	 * @param constructeur Le constructeur ou <code>null</code>;
	 * @param methodes Une {@link HashMap} des méthodes.
	 * @param champsStatiques Une {@link HashMap} des champs statiques.
	 * @param estAbstraite
	 */
	public AClasse(String nom,
			RClasse[] parentes,
			RObjet constructeur,
			HashMap<String, RObjet> methodes,
			HashMap<String, RObjet> champsStatiques,
			boolean estAbstraite) {

		super(parentes, methodes, champsStatiques, estAbstraite, false);

		this.nom = nom;
		this.constructeur = constructeur;
		classeJava = null;
	}

	/**
	 * Crée une nouvelle instance de la classe.
	 * @return La nouvelle instance de la classe ou <code>null</code>, si la
	 * classe est abstraite.
	 * 
	 * @param moi Paramètre ignoré, n’a pas de sens dans un constructeur.
	 */
	@Override
	public AObjet appeler(RObjet moi, RObjet[] params) {

		if (estAbstraite) {
			throw new AException(
					"Une classe abstraite ne peut être instanciée").getRException();
		} else {

			if (classeJava != null) {

				return (AObjet) constructeur.appeler(moi, params);

			} else {
				if (moi == RRien.RIEN)
					return construireObjetScript(params);
				else {
					if (! (moi instanceof AObjet))
						throw new RuntimeException();

					// L’objet existe déjà, c’est un appel de constructeur
					// de superclasse
					constructeur.appeler(moi, params);
					return (AObjet) moi;
				}
			}
		}
	}

	//-------------------------------------------------------------------------

	/**
	 * Créé un nouvel {@link AObjet} puis appelle le constructeur sur cet
	 * objet, si cette classe est écrite en smallFrench
	 * 
	 * @param params Les paramètres du constructeur.
	 * @return La nouvelle instance de la classe
	 * @throws RuntimeException
	 */
	private AObjet construireObjetScript(RObjet[] params) {

		HashMap<Class<?>, AObjet> supersObjetsNatifs =
				new HashMap<Class<?>, AObjet>();

		ArrayList<AClasse> superClassesNatives = getSuperClassesNatives();

		for (AClasse i : superClassesNatives) {
			AObjet o = i.appeler(RRien.RIEN, params);
			supersObjetsNatifs.put(i.classeJava, o);
		}

		if (constructeur == null)
			return new AObjet(this, supersObjetsNatifs);

		AObjet no = new AObjet(this, supersObjetsNatifs);
		constructeur.appeler(no, params);
		return no;
	}

	//-------------------------------------------------------------------------

	/**
	 * @return Le nom de la classe.
	 */
	@Override
	public RChaine enChaine() {
		return new RChaine(nom);
	}


	/**
	 * Fonction récursive récupérant les superclasses natives de cette classe.
	 * 
	 * @return Une liste des superclasses natives
	 */
	private ArrayList<AClasse> getSuperClassesNatives() {

		ArrayList<AClasse> supersNatives = new ArrayList<AClasse>();

		for (RClasse parente : parentes) {

			if (parente instanceof AClasse) {

				AClasse ac = (AClasse) parente;

				if (ac.classeJava != null) {
					supersNatives.add(ac);
				} else {
					// ac n’est pas native, mais ses superclasses le sont
					// forcément
					supersNatives.addAll(ac.getSuperClassesNatives());
				}
			}
		}

		return supersNatives;
	}

	/**
	 * Encapsule un constructeur Java
	 */
	public static class AConstructeur extends RFonction {

		private final Constructor<?> constructor;

		private final boolean parametresVariables;

		public AConstructeur(Constructor<?> constructor,
				boolean parametresVariables) {

			this.constructor = constructor;
			this.parametresVariables = parametresVariables;
		}

		/**
		 * Crée un nouvel objet.
		 * @param moi Paramètre ignoré.
		 */
		@Override
		public RObjet appeler(RObjet moi, RObjet[] params) {

			try {

				Object r;
				if (parametresVariables)
					r = constructor.newInstance((Object)params);
				else
					r = constructor.newInstance((Object[]) params);
				return (AObjet) r;

			} catch (InstantiationException e) {
				e.printStackTrace();

			} catch (IllegalAccessException e) {
				e.printStackTrace();

			} catch (IllegalArgumentException e) {
				String descr = getDescrErreurConstructeur(constructor);
				new ErreurParametres(descr).lancer();

			} catch (InvocationTargetException e) {

				Throwable cause = e.getCause();
				if (cause instanceof RException) {
					// Tout va bien :-)
					throw (RException) cause;
				}

				e.printStackTrace();
				System.err.println("Cause :");
				cause.printStackTrace();
			}
			return RRien.RIEN;
		}

		@Override
		public RChaine enChaine() {
			return new RChaine("Constructeur");
		}
	}

	/**
	 * Encapsule une méthode de classe native ; statique ou non.
	 */
	public static class AMethode extends RFonction {

		/**
		 * La méthode Java
		 * */
		private final Method method;

		private final boolean parametresVariables;

		private final boolean statique;

		/**
		 * 
		 * @param method La méthode Java
		 * @param parametresVariables
		 */
		public AMethode(Method method, boolean parametresVariables) {
			statique = Modifier.isStatic(method.getModifiers());
			this.method = method;
			this.parametresVariables = parametresVariables;
		}

		/**
		 * @param ro Doit être un {@link AObjet}, une instance de la classe
		 * smallFrench possédant cette méthode ; ou dont l’une des classes
		 * parentes possède cette méthode. Ignoré si la méthode est statique.
		 * @param params
		 */
		@Override
		public RObjet appeler(RObjet ro, RObjet[] params) {

			try {

				AObjet moi = getSuperObjetNatif(ro);

				Object resultat;

				if (parametresVariables)
					resultat = method.invoke(moi, (Object)params);
				else
					resultat = method.invoke(moi, (Object[]) params);

				return (RObjet) resultat;

			} catch (IllegalAccessException e) {

				System.err.println(getDescrErreurMethode(method));
				e.printStackTrace();

				new AException(getDescrErreurMethode(method)).lancer();

			} catch (IllegalArgumentException e) {

				//	System.err.println(getDescrErreurMethode(method));
				//	e.printStackTrace();
				new ErreurParametres(getDescrErreurMethode(method)).lancer();

			} catch (InvocationTargetException ite) {

				Throwable cause = ite.getCause();

				if (cause instanceof RException) {

					RException re = (RException) cause;
					AException ae = re.exception;

					if (ro == ae) {
						// On throw ro au cas où ro est l’instance d’une
						// classe dérivée de err.Exception
						throw new RException((AObjet) ro);
					}

					throw re;
				}

				System.err.println(getDescrErreurMethode(method));
				ite.printStackTrace();

				System.err.println("Cause :");
				cause.printStackTrace();

			}

			return RRien.RIEN;
		}

		@Override
		public RChaine enChaine() {
			return new RChaine("Methode");
		}

		/**
		 * 
		 * @param ro
		 * @return Soit ro, soit un objet parent de ro contenant la méthode.
		 * Si la méthode est statique, <code>null</code> est retourné.
		 */
		private AObjet getSuperObjetNatif(RObjet ro) {

			if (statique)
				return null;

			if (! (ro instanceof AObjet))
				throw ErreurClasse.nouvelleException(ro, "objet non natif");

			AObjet moi = (AObjet) ro;

			if (! moi.classe.estIntegree) {
				//La classe n’est pas native
				Class<?> classeJava = method.getDeclaringClass();

				moi = moi.supersObjetsNatifs.get(classeJava);
				if (moi == null)
					throw new RuntimeException();
			}

			return moi;
		}
	}


}
